/*
 * Copyright (c) 2012, marco.tamburelli@gmail.com
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met: 
 * 
 * 1. Redistributions of source code must retain the above copyright notice, this
 *    list of conditions and the following disclaimer. 
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution. 
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package org.bmi.gwt.mi.shared;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

import org.bmi.gwt.mi.shared.communication.call.Call;
import org.bmi.gwt.mi.shared.communication.call.TrCommitCall;
import org.bmi.gwt.mi.shared.exceptions.InvalidArgumentException;
import org.bmi.gwt.mi.shared.exceptions.NotAvailableConstructorException;
import org.bmi.gwt.mi.shared.util.ProxyBase;
import org.bmi.gwt.mi.shared.util.UtilityFunctions;

/**
 * TODO Valid class description
 * 
 * @author marco.tamburelli@gmail.com
 */
public abstract class MiTransaction
{
	protected LinkedList<Call> calls;
	protected String transactionId;

	protected BroadChannel broadChannel;

	protected MiTransaction(BroadChannel broadChannel)
	{
		this.broadChannel = broadChannel;

		this.transactionId = UtilityFunctions.generateUniqueId();
		this.calls = new LinkedList<Call>();
	}

	/**
	 * Registers a call.
	 * 
	 * @param call
	 */
	protected void addCall(Call call)
	{
		this.calls.add(call);
	}

	/**
	 * Fire the transaction, launching every call executed till now.
	 * 
	 * The transaction if not consumed immediately by some bound peer will
	 * expire.
	 */
	public void commit()
	{
		TrCommitCall commit = new TrCommitCall();
		commit.setTransactionId(this.transactionId);
		this.calls.add(commit);

		publish(this.calls);

		/*
		 * The list given to the publisher should not be altered, to avoid this
		 * a new list will be created for the next execution.
		 */
		this.calls = new LinkedList<Call>();
	}

	/**
	 * Fire the transaction, launching every call executed till now.
	 * 
	 * @param timeToLive The number of milliseconds the transaction can wait on
	 *        the queue before expiring.
	 */
	public void commit(long timeToLive)
	{
		assert timeToLive > 0 : "The expiration time of the transaction must be greather than 0.";
		ArrayList<Call> _calls = new ArrayList<Call>(this.calls.size());

		for (Call call : this.calls)
		{
			call.setTimeToLive(timeToLive);
			_calls.add(call);
		}

		TrCommitCall commit = new TrCommitCall();
		commit.setTransactionId(this.transactionId);
		commit.setTimeToLive(timeToLive);

		_calls.add(commit);

		publish(_calls);

		/*
		 * The list given to the publisher should not be altered, to avoid this
		 * a new list will be created for the next execution.
		 */
		this.calls = new LinkedList<Call>();
	}

	/**
	 * Clear each registered call.
	 */
	public void rollback()
	{
		this.calls.clear();
	}

	/**
	 * This method returns a generated proxy which pushes methods to clients.
	 * Calling this method provokes the creation of an element on each listening
	 * client.<br>
	 * 
	 * Note that this way to launch the creation of an object could be slow.
	 * Creating locally an object and registering it has better performance.
	 * 
	 * @param proxyType An interface class extending {@link MiProxy} and
	 *        defining methods to be pushed.
	 * @param constructorArgs Arguments to be pushed to the constructor.
	 * @return A proxy object implementing the provided interface extending
	 *         {@link MiProxy}.
	 * @throws InvalidArgumentException When the target object doesn't allow a
	 *         constructor compatible with provided parameters
	 */
	@SuppressWarnings("unchecked")
	public <T extends MiProxy> T proxyToNewObject(Class<T> proxyType, Object... constructorArgs)
			throws InvalidArgumentException
	{
		String objectId = UtilityFunctions.generateUniqueId();

		MiTransactionPublisher publisher = (MiTransactionPublisher) this.broadChannel.provideMiPublisher(true);
		publisher.setBroadChannel(this.broadChannel);
		publisher.setTransaction(this);

		try
		{
			ProxyBase proxy = this.broadChannel.proxyFactory.createProxy(proxyType, constructorArgs);

			ProxyBase.setObjectId(proxy, objectId);
			ProxyBase.setPublisher(proxy, publisher);
			ProxyBase.publishConstructor(proxy, constructorArgs);

			return (T) proxy;
		}
		catch (NotAvailableConstructorException e)
		{
			throw new InvalidArgumentException("Provided arguments don't match any provided constructor", e);
		}
	}

	/**
	 * This method returns a proxy which pushes methods to a known object bound
	 * to one. To avoid unpredictable behaviors the provided object id should be
	 * a valid id from an already created object.
	 * 
	 * @param proxyType An interface class extending {@link MiProxy} and
	 *        defining methods to be pushed.
	 * @param objectId The unique id of an object.
	 * @return A proxy object implementing the provided interface extending
	 *         {@link MiProxy}.
	 */
	@SuppressWarnings("unchecked")
	public <T extends MiProxy> T proxyToObject(Class<T> proxyType, String objectId)
	{
		MiTransactionPublisher publisher = (MiTransactionPublisher) this.broadChannel.provideMiPublisher(true);
		publisher.setBroadChannel(this.broadChannel);
		publisher.setTransaction(this);

		ProxyBase proxy = this.broadChannel.proxyFactory.createProxy(proxyType);
		ProxyBase.setObjectId(proxy, objectId);
		ProxyBase.setPublisher(proxy, publisher);

		return (T) proxy;
	}

	/**
	 * Add the given proxy to a transactional environment defined by this
	 * transaction.
	 * 
	 * @param proxy
	 */
	public <T extends MiProxy> void addToTransaction(T proxy)
	{
		MiTransactionPublisher publisher = (MiTransactionPublisher) this.broadChannel.provideMiPublisher(true);
		publisher.setBroadChannel(this.broadChannel);
		publisher.setTransaction(this);

		ProxyBase.setPublisher((ProxyBase) proxy, publisher);
	}

	/**
	 * Removes the given proxy to any previously provided transaction.
	 * 
	 * @param proxy
	 */
	public <T extends MiProxy> void removeFromTransaction(T proxy)
	{
		MiPublisher publisher = this.broadChannel.provideMiPublisher(false);
		publisher.setBroadChannel(this.broadChannel);

		ProxyBase.setPublisher((ProxyBase) proxy, publisher);
	}

	/**
	 * Publishes the provided method calls.
	 * 
	 * @param calls
	 */
	protected abstract void publish(List<Call> calls);
}
